package org.kca.yarmour.data;

import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.kca.yarmour.YAException;
import org.kca.yarmour.data.GlobalData.CargoType;
import org.kca.yarmour.data.GlobalData.FactionType;
import org.kca.yarmour.data.GlobalData.TerrainType;
import org.kca.yarmour.data.GlobalData.WeatherType;
import org.kca.yarmour.data.Quest.QuestStatus;
import org.kca.yarmour.utils.Pair;
import org.kca.yarmour.view.GameTime;
import org.kca.yarmour.yadata.EntryType;
import org.kca.yarmour.yadata.ItemType;
import org.kca.yarmour.yadata.TableType;
import org.kca.yarmour.yadata.VehicleStatsType;
import org.kca.yarmour.yadata.YADataFileType;
import org.kca.yarmour.yaquest.QuestListType;

/**
 * This is the master class that contains all game state information
 * @author ctheng
 *
 */
public class GameState
{
    private WorldMap worldMap;
    private Player player;
    private Random rng;
    
    /* Data */
    private Map<String, VehicleStats> vehicles;
    private Map<String, InvItem> itemDB;
    private Map<String, QuestDetails> questDB;
    
    /* Environmental stats */
    private int psi, tide;
    private WeatherType weather;
    private GameTime gameTime;
    
    private PriorityQueue<TimeEvent> queue;
    
    public GameState()
    {
        rng = new Random();
    }
    
    public void init()
    {
        String filename = "res/xml/data4.xml";
        String qfilename = "res/xml/quest1.xml";
        /*
         * 1. Load XML file
         * 2. Initialize global constants
         * 3. Initialize world map
         * 4. Initialize vehicles
         * 5. Initialize player
         * 6. Initialize items
         * 7. Initialize quests
         * 8. Initialize environment
         * 9. Initialize game time
         */
        YADataFileType yadata = loadXmlData(filename);
        QuestListType yaquest = loadXmlQuests(qfilename);
        initGlobalConstants(yadata);
        worldMap = new WorldMap(GlobalData.MAP_ROWS, GlobalData.MAP_COLS);
        worldMap.initMap(yadata.getLocation(), yadata.getTown());
        initTownPrices();
        vehicles = new HashMap<String, VehicleStats>();
        for (VehicleStatsType xmlStats : yadata.getVehicle())
        {
            VehicleStats v = VehicleStats.createFromXmlType(xmlStats);
            vehicles.put(v.getId(), v);
        }
        player = new Player();
        // TODO: Test code, remove before going live
        player.setRow(10);
        player.setCol(12);
        player.setFacing(GlobalData.DIR_RIGHT);
        player.setVehicle(vehicles.get("v_bx"));
        player.restoreHull();
        // TODO: End of test code
        
        // Initialize items
        itemDB = new HashMap<String, InvItem>();
        for (ItemType itype : yadata.getItem())
        {
            InvItem item = InvItem.createFromXmlType(itype);
            if (itemDB.containsKey(item.getId()))
            {
                System.err.println("[WARN] Duplicate item with id " + item.getId());
                continue;
            }
            itemDB.put(item.getId(), item);
        }
        
        // Initialize quests - read from file
        questDB = new HashMap<String, QuestDetails>();
        for (org.kca.yarmour.yaquest.QuestType qtype : yaquest.getQuest())
        {
            QuestDetails q = QuestDetails.createFromXmlType(qtype);
            if (questDB.containsKey(q.getId()))
            {
                System.err.println("[WARN] Duplicate quest with id " + q.getId());
                continue;
            }
            questDB.put(q.getId(), q);
            // Tie quests to start location
            String[] locs = q.getSpawnLocation().split(";");
            for (String loc : locs)
            {
                WorldLocation wloc = worldMap.getLocation(loc);
                if (wloc != null)
                {
                    wloc.getLocalQuests().add(q.getId());
                }
            }
        }
        
        
        // Initialize environment and game time
        psi = 100;
        tide = 1;
        weather = WeatherType.CLEAR;
        gameTime = new GameTime();
        queue = new PriorityQueue<TimeEvent>();
        Calendar startTime = (Calendar)gameTime.getTime().clone();
        startTime.add(Calendar.HOUR, GlobalData.WEATHER_CHANGE_HOURS);
        // Queue next weather change
        queue.offer(new TimeEvent(startTime, GlobalData.EVENT_WEATHER_CHANGE));
    }
    
    private void initGlobalConstants(YADataFileType data)
    {
        TableType tt = getTableByName(data, GlobalData.TERRAIN_TRAVEL_TABLE);
        if (tt == null)
        {
            throw new YAException("Terrain travel table missing from YAdata XML.");
        }
        TerrainType.initializeTerrain(tt);
        
    }
    
    /**
     * This method initializes all the prices in each town
     */
    private void initTownPrices()
    {
        // For now, sets the price to N~(30, 5)
        for (Town t : worldMap.getTowns())
        {
            t.initSupply();
            for (int i = 0; i < 7; i++)
            {
                t.setPrice(i, (int)Math.round(30 + rng.nextGaussian() * 5), 0.9f); 
            }
        }
    }
    
    /**
     * Gets the world map
     * @return the world map
     */
    public WorldMap getWorldMap()
    {
        return worldMap;
    }
    
    /**
     * Gets the player
     * @return the player
     */
    public Player getPlayer()
    {
        return player;
    }
    
    /**
     * Gets the current game time
     * @return the game time
     */
    public GameTime getGameTime()
    {
        return gameTime;
    }
    
    /**
     * Gets the current weather type
     * @return the current weather
     */
    public WeatherType getWeather()
    {
        return weather;
    }
    
    /**
     * Sets the current weather type
     * @param w the weather to set
     */
    public void setWeather(WeatherType w)
    {
        weather = w;
    }
    
    /**
     * This method changes the weather. Currently, the method of weather randomization is<br>
     * <ul><li>Weather is changed uniformly</li>
     * <li>Tide alternates between low and high</li>
     * <li>PSI is a random integer in a uniform distribution between 1 and 500</li>
     * @return a message that can be displayed on the notification box
     */
    public String changeWeather()
    {
        WeatherType nextWeather = WeatherType.values()[rng.nextInt(WeatherType.values().length)];
        int nextTide = (getTide() >= GlobalData.HIGH_TIDE_THRESHOLD) ? 
                GlobalData.HIGH_TIDE_THRESHOLD - 1 : GlobalData.HIGH_TIDE_THRESHOLD;
        int nextPsi = rng.nextInt(500) + 1;
        setWeather(nextWeather);
        setTide(nextTide);
        setPsi(nextPsi);
        return gameTime.toString() + ": The weather is now " + nextWeather.getName();
    }
    
    /**
     * Gets the current tide
     * @return the current tide
     */
    public int getTide()
    {
        return tide;
    }
    
    /**
     * Sets the current tide
     * @param t the tide to set
     */
    public void setTide(int t)
    {
        tide = t;
    }
    
    /**
     * Gets the current PSI
     * @return the current PSI
     */
    public int getPsi()
    {
        return psi;
    }
    
    /**
     * Sets the current PSI
     * @param p the PSI to set
     */
    public void setPsi(int p)
    {
        psi = p;
    }
    
    /**
     * Gets the event queue
     * @return the event queue
     */
    public PriorityQueue<TimeEvent> getEventQueue()
    {
        return queue;
    }
    
    /**
     * Gets the player's current location
     * @return the current location
     */
    public WorldLocation getCurrentLocation()
    {
        return worldMap.getLocation(player.getRow(), player.getCol());
    }
    
    /**
     * This method is called to move the player. It will check for available quests at 
     * the player's new location
     * @param row the new row coordinate
     * @param col the new col coordinate
     */
    public void movePlayer(int row, int col)
    {
        player.setRow(row);
        player.setCol(col);
        WorldLocation wloc = getCurrentLocation();
        updateAvailableQuests(wloc);
    }
    
    /**
     * Checks if player is on a location with a town
     * @return true if player is on a town
     */
    public boolean onTown()
    {
        return getCurrentLocation().getTown() != null;
    }
    
    /**
     * Gets the quest corresponding to an id
     * @param id the quest id
     * @return the quest of that id, or null if not found
     */
    public QuestDetails getQuest(String id)
    {
        return questDB.get(id);
    }
    
    /**
     * Gets the item corresponding to an id
     * @param id the item id
     * @return the item with that id, or null if not found
     */
    public InvItem getInvItem(String id)
    {
        return itemDB.get(id);
    }
    
    /**
     * Checks through the list of potentially available quests at the location and
     * adds to the available quest list. 
     * @param wloc the location to check in
     */
    private void updateAvailableQuests(WorldLocation wloc)
    {
        for (String qid : wloc.getLocalQuests())
        {
            QuestDetails qd = questDB.get(qid);
            if (checkPrerequisites(qd) && !wloc.hasAvailableQuest(qid))
            {
                wloc.getAvailableQuests().add(new Quest(qd, QuestStatus.AVAILABLE));
            }
        }
    }
    
    /**
     * Checks if the player has the prerequisites needed to accept a quest
     * @param qd the quest to check for
     * @return true if the prerequisites have been met
     */
    private boolean checkPrerequisites(QuestDetails qd)
    {
        for (Info prereq : qd.getPrereqs())
        {
            String type = prereq.getType();
            String value = prereq.getValue();
            if (type == null || value == null)
            {
                System.err.println("[WARN] Quest " + qd.getId() + " has prerequisites with " +
                		"no type or value.");
                continue;
            }
            if (type.equalsIgnoreCase("Completed Quest"))
            {
                if (player.getQuestCompletionCount(value) < prereq.getAmountSafely(1))
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Flag"))
            {
                if (!player.checkFlag(value))
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Unflag"))
            {
                if (player.checkFlag(value))
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Achievement"))
            {
                //TODO: To implement when achievements are added
            }
            else if (type.equalsIgnoreCase("Have Cargo"))
            {
                CargoType ctype = CargoType.getCargo(value);
                if (ctype == null || !prereq.isAmountSpecified())
                {
                    System.err.println("[WARN] Quest " + qd.getId() + " has Have Cargo prereq " +
                            "with invalid parameters");
                    continue;
                }
                if (player.getCargo(ctype.ordinal()) < prereq.getAmount())
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Have Item"))
            {
                if (!player.hasItem(value, prereq.getAmountSafely(1)))
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Have Money"))
            {
                if (!prereq.isAmountSpecified())
                {
                    System.err.println("[WARN] Quest " + qd.getId() + " has Have Money prereq " +
                            "with invalid parameters");
                    continue;
                }
                if (player.getMoney() < prereq.getAmount())
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Faction Rep"))
            {
                FactionType ftype = FactionType.getFaction(value);
                if (ftype == null || !prereq.isAmountSpecified())
                {
                    System.err.println("[WARN] Quest " + qd.getId() + " has Faction Rep prereq " +
                            "with invalid parameters");
                    continue;
                }
                if (player.getReputation(ftype.ordinal()) < prereq.getAmount())
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("Weather"))
            {
                String[] tokens = value.split(",");
                boolean found = false;
                for (String tok : tokens)
                {
                    WeatherType wt = WeatherType.getWeather(tok);
                    if (wt == null)
                    {
                        System.err.println("[WARN] Quest " + qd.getId() + " has Weather prereq " +
                        		"with invalid weather type: " + tok);
                        continue;
                    }
                    if (wt.equals(getWeather()))
                    {
                        found = true;
                        break;
                    }
                }
                if (!found)
                {
                    return false;
                }
            }
            else if (type.equalsIgnoreCase("PSI"))
            {
                if (!prereq.isAmountSpecified())
                {
                    System.err.println("[WARN] Quest " + qd.getId() + " has PSI prereq " +
                            "with invalid parameters");
                    continue;
                }
                if (value.equalsIgnoreCase("less than"))
                {
                    if (getPsi() >= prereq.getAmount())
                    {
                        return false;
                    }
                }
                else if (value.equalsIgnoreCase("more than"))
                {
                    if (getPsi() <= prereq.getAmount())
                    {
                        return false;
                    }
                }
                else
                {
                    System.err.println("[WARN] Quest " + qd.getId() + " has PSI prereq " +
                            "with invalid parameters");
                    continue;
                }
            }
            
        }
        return true;
    }
    
    /**
     * Loads XML data from a file
     * @param filename the file name
     * @return the XML data file
     */
    private YADataFileType loadXmlData(String filename)
    {
        JAXBContext jaxb;
        try
        {
            jaxb = JAXBContext.newInstance("org.kca.yarmour.yadata");
            Unmarshaller u = jaxb.createUnmarshaller();
            JAXBElement<YADataFileType> yadata = (JAXBElement<YADataFileType>)u.unmarshal(new File(filename));
            return yadata.getValue();
        } 
        catch (JAXBException e)
        {
            throw new YAException(e);
        }
    }
    
    /**
     * Loads XML quests from a file
     * @param filename the file name
     * @return the XML quest file
     */
    private QuestListType loadXmlQuests(String filename)
    {
        JAXBContext jaxb;
        try
        {
            jaxb = JAXBContext.newInstance("org.kca.yarmour.yaquest");
            Unmarshaller u = jaxb.createUnmarshaller();
            JAXBElement<QuestListType> yaquest = (JAXBElement<QuestListType>)u.unmarshal(new File(filename));
            return yaquest.getValue();
        } 
        catch (JAXBException e)
        {
            throw new YAException(e);
        }
    }
    
    /**
     * Gets a table from XML data by name
     * @param yadata the XML data file
     * @param name the table name
     * @return the first table of that name if it exists, null otherwise
     */
    private static TableType getTableByName(YADataFileType yadata, String name)
    {
        for (TableType tt : yadata.getTable())
        {
            if (tt.getName().equals(name))
            {
                return tt;
            }
        }
        return null;
    }
    
    /**
     * Gets an entry from an XML TableType by key
     * @param table the XML Table object
     * @param key the entry key
     * @return the first entry of that key if it exists, null otherwise
     */
    private static EntryType getEntryByKey(TableType table, String key)
    {
        for (EntryType et : table.getEntry())
        {
            if (et.getKey().equals(key))
            {
                return et;
            }
        }
        return null;
    }

    /**
     * Returns true if player can access a secret location. Currently always
     * false as secret areas are not yet implemented
     * @return true if player can access a secret location, false otherwise
     */
    public boolean onSecretLocation()
    {
        //TODO: implement this
        return false;
    }
    
    public static class TimeEvent implements Comparable<TimeEvent>
    {
        private Calendar time;
        private int eventCode;
        private Object data;
        
        /**
         * Creates a new Time event with specified event code that will occur 
         * at the specified time
         * @param time the time of the event
         * @param eventCode the event code
         */
        public TimeEvent(Calendar time, int eventCode)
        {
            this.time = time;
            this.eventCode = eventCode;
        }

        /**
         * Gets the time
         * @return the time
         */
        public Calendar getTime()
        {
            return time;
        }

        /**
         * Sets the time
         * @param time the time to set
         */
        public void setTime(Calendar time)
        {
            this.time = time;
        }

        /**
         * Gets the eventCode
         * @return the eventCode
         */
        public int getEventCode()
        {
            return eventCode;
        }

        /**
         * Sets the eventCode
         * @param eventCode the eventCode to set
         */
        public void setEventCode(int eventCode)
        {
            this.eventCode = eventCode;
        }

        /**
         * Gets the additional data associated with the event
         * @return the data
         */
        public Object getData()
        {
            return data;
        }

        /**
         * Sets the additional data associated with the event
         * @param data the data to set
         */
        public void setData(Object data)
        {
            this.data = data;
        }

        @Override
        public int compareTo(TimeEvent te)
        {
            return time.compareTo(te.getTime());
        }
    }
}
